/*
 * Copyright (c) 2007 Team and Concepts Limited.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Team and Concepts Limited nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY TEAM AND CONCEPTS LIMITED ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TEAM AND CONCEPTS LIMITED BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Text;
using EditGrid.API.Client.Atom;
using System.Net;
using System.IO;

namespace EditGrid.API.Client
{
    /// <summary>
    /// Creates a EditGrid API 2.0 service
    /// </summary>
    /// <example>This example shows how to sign on API 2.0 and retrieve a list of
    /// <c>Spreadsheet</c> from the authenticated user's workspace.
    /// <code>
    /// EditGridService service = new EditGridService(appId);
    /// service.Authenticate("user/alice", "password");
    /// List<Spreadsheet> list = service.ListSpreadsheet("_me");
    /// </code>
    /// </example>
    public class EditGridService
    {
        private string appId;
        private string apiToken;
        private Uri baseUri;

        /// <summary>
        /// Returns the Application ID.
        /// </summary>
        /// <value>The <paramref name="appId"/> that was pased into the
        /// <c>EditGridService</c> constructor.</value>
        public string AppId
        {
            get { return this.appId; }
        }

        /// <summary>
        /// Returns the API Token.
        /// </summary>
        /// <value>The API Token obtained from EditGrid API 2.0 service.
        /// The value is a null reference if <c>Authenticate</c> is not
        /// successfully called.</value>
        public string ApiToken
        {
            get { return this.apiToken; }
        }

        /// <summary>
        /// Returns the URI of EditGrid installation.
        /// </summary>
        public Uri BaseUri
        {
            get { return this.baseUri; }
        }

        /// <summary>
        /// Gets or sets the proxy server to use by EditGridService.
        /// </summary>
        public static IWebProxy Proxy
        {
            get { return EditGridHttpRequest.Proxy; }
            set { EditGridHttpRequest.Proxy = value; }
        }

        /// <summary>
        /// Gets or sets the HTTP credentials to use by EditGridService.
        /// </summary>
        public static ICredentials Credentials
        {
            get { return EditGridHttpRequest.Credentials; }
            set { EditGridHttpRequest.Credentials = value; }
        }

        /// <summary>
        /// Initializes a new instance of the <c>EditGridService</c> class.
        /// </summary>
        /// <param name="appId">A valid Application ID obtain from EditGrid.</param>
        /// <param name="baseUri">An <c>URI</c> class than contains the address of
        /// EditGrid installation.</param>
        public EditGridService(string appId, Uri baseUri)
        {
            this.appId = appId;
            this.baseUri = baseUri;
        }

        /// <summary>
        /// Initializes a new instance of the <c>EditGridService</c> class that
        /// connects to the default EditGrid installation.
        /// </summary>
        /// <param name="appId">A valid Application ID obtain from EditGrid.</param>
        public EditGridService(string appId)
            : this(appId, new Uri(Properties.Settings.Default.DefaultEditGridURL))
        {
        }

        /// <summary>
        /// Obtains an API Token from EditGrid API 2.0 service, without user authentication.
        /// </summary>
        public void Authenticate()
        {
            this.apiToken = Authentication.GetToken(this.baseUri, this.appId);
        }

        /// <summary>
        /// Obtains an API Token from EditGrid API 2.0 service, using Auth Key authentication.
        /// </summary>
        /// <param name="authKey">A valid Auth Key</param>
        public void Authenticate(string authKey)
        {
            this.apiToken = Authentication.GetToken(this.baseUri, this.appId, authKey);
        }

        /// <summary>
        /// Obtains an API Token from EditGrid API 2.0 service, using password authentication.
        /// </summary>
        /// <param name="login">Login name of the user</param>
        /// <param name="password">Password of the user</param>
        public void Authenticate(string login, string password)
        {
            this.apiToken = Authentication.GetToken(this.baseUri, this.appId, login, password);
        }


        /// <summary>
        /// Obtains an API Token from EditGrid API 2.0 service, using password authentication.
        /// </summary>
        /// <param name="organisation">Organisation of the user</param>
        /// <param name="login">Login name of the user</param>
        /// <param name="password">Password of the user</param>
        public void Authenticate(string organisation, string login, string password)
        {
            this.Authenticate(organisation + "/" + login, password);
        }

        /// <summary>
        /// Obtains an Auth Key from EditGrid API 2.0 service
        /// </summary>
        /// <param name="login">Login name of the user</param>
        /// <param name="password">Password of the user</param>
        /// <returns>The Auth Key</returns>
        public string GetAuthKey(string login, string password)
        {
            return Authentication.GetAuthKey(this.baseUri, this.appId, login, password);
        }

        /// <summary>
        /// Obtains an Auth Key from EditGrid API 2.0 service
        /// </summary>
        /// <param name="organisation">Organisation of the user</param>
        /// <param name="login">Login name of the user</param>
        /// <param name="password">Password of the user</param>
        /// <returns>The Auth Key</returns>
        public string GetAuthKey(string organisation, string login, string password)
        {
            return this.GetAuthKey(organisation + "/" + login, password);
        }

        /// <summary>
        /// Revokes the API Token obtained previously.
        /// </summary>
        public void Unauthenticate()
        {
            if (this.apiToken != null)
            {
                Authentication.RevokeToken(this.baseUri, this.appId, this.apiToken);
                this.apiToken = null;
            }
        }

        /// <summary>
        /// Returns an instance of <c>User</c> class containing the metadata of the authenticated user.
        /// </summary>
        /// <returns>An <c>User</c> object.</returns>
        public User GetAuthenticatedUser()
        {
            AtomEntry entry = this.QueryEntry("users/_me");
            return new User(entry);
        }

        #region Basic APP Operation
        /// <summary>
        /// Create resource request.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="entry">The <c>AtomEntry</c> to be created</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource created</returns>
        public AtomEntry Create(string path, AtomEntry entry)
        {
            return this.Create(path, entry, new QueryOption());
        }      

        /// <summary>
        /// Create resource request with <c>QueryOption</c>.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="entry">>The <c>AtomEntry</c> to be created</param>
        /// <param name="option">The query options of the request</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource created</returns>
        public AtomEntry Create(string path, AtomEntry entry, QueryOption option)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/" + path));
            ub.Query = option.ToString();
            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            req.SetRequestBody(entry.ToAtomAsString());
            req.ContentType = "application/atom+xml";
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode == HttpStatusCode.Created)
            {
                return res.ResponseAtomEntry;
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }

        /// <summary>
        /// Update resource request.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="entry">The <c>AtomEntry</c> used to update an existing resource.</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource updated</returns>
        public AtomEntry Update(string path, AtomEntry entry)
        {
            return this.Update(path, entry, new QueryOption());
        }

        /// <summary>
        /// Update resource request with <c>QueryOption</c>.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="entry">The <c>AtomEntry</c> used to update an existing resource.</param>
        /// <param name="option">The query options of the request</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource updated</returns>
        public AtomEntry Update(string path, AtomEntry entry, QueryOption option)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/" + path));
            ub.Query = option.ToString();
            EditGridHttpRequest req = new EditGridHttpRequest("PUT", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            req.SetRequestBody(entry.ToAtomAsString());
            req.ContentType = "application/atom+xml";
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode == HttpStatusCode.OK)
            {
                return res.ResponseAtomEntry;
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }

        /// <summary>
        /// Retrieve resource request.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resouce requested</returns>
        public AtomEntry QueryEntry(string path)
        {
            return this.QueryEntry(path, new QueryOption());
        }

        /// <summary>
        /// Retrieve resource request with <c>QueryOption</c>.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="option">The query options of the request</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resouce requested</returns>
        public AtomEntry QueryEntry(string path, QueryOption option)
        {
            AtomFeed feed = this.Query(path, option);
            return feed.Entries[0];
        }

        /// <summary>
        /// Retrieve resource list request.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <returns>A list of <c>AtomEntry</c> objects contain metadata of the resouce requested</returns>
        public AtomFeed Query(string path)
        {
            return this.Query(path, new QueryOption());
        }

        /// <summary>
        /// Retrieve resource list request with <c>QueryOption</c>.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="option">The query options of the request</param>
        /// <returns>A list of <c>AtomEntry</c> objects contain metadata of the resouce requested</returns>
        public AtomFeed Query(string path, QueryOption option)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/" + path));
            ub.Query = option.ToString();
            EditGridHttpRequest req = new EditGridHttpRequest("GET", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode == HttpStatusCode.OK)
            {
                return res.ResponseAtomFeed;
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }

        /// <summary>
        /// Delete resource request.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource deleted</returns>
        public AtomEntry Delete(string path)
        {
            return this.Delete(path, new QueryOption());
        }

        /// <summary>
        /// Delete resource request with <c>QueryOption</c>.
        /// </summary>
        /// <param name="path">Request path without the "rest/1/" prefix</param>
        /// <param name="option">The query options of the request</param>
        /// <returns>An <c>AtomEntry</c> object contains metadata of the resource deleted</returns>
        public AtomEntry Delete(string path, QueryOption option)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/" + path));
            ub.Query = option.ToString();
            EditGridHttpRequest req = new EditGridHttpRequest("DELETE", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode == HttpStatusCode.OK)
            {
                return res.ResponseAtomEntry;
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }
        #endregion

        /// <summary>
        /// Call EditGrid Action API
        /// </summary>
        /// <param name="actionPath">Action path</param>
        /// <param name="parameters">Parameters passing to Action API service</param>
        /// <returns></returns>
        public EditGridHttpResponse CallAction(string actionPath, Dictionary<string, string> parameters)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/action/" + actionPath));
            QueryOption option = new QueryOption(parameters);
            ub.Query = option.ToString();
            EditGridHttpRequest req = new EditGridHttpRequest("GET", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            EditGridHttpResponse res = req.GetResponse();

            if (res.StatusCode == HttpStatusCode.OK)
            {
                return res;
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }

        /// <summary>
        /// Create a spreadsheet in the workspace specified by <paramref name="workspacePath"/> and public
        /// access right specified by <paramref name="publicAccessRight"/>
        /// </summary>
        /// <param name="workspacePath">The path of the destined workspace.</param>
        /// <param name="spreadsheetName">The name of the spreadsheet to be created.</param>
        /// <param name="note">The note of the spreadsheet to be created.</param>
        /// <param name="publicAccessRight">Public access right of the spreadsheet to be created.</param>
        /// <returns>A <c>Spreadsheet</c> object.</returns>
        public Spreadsheet CreateSpreadsheet(string workspacePath, string spreadsheetName, string note,
            PublicAccessRight right)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("spreadsheet-name", spreadsheetName);
            entry.SetValue("note", note);
            Spreadsheet.SetPublicAccessRightInAtomEntry(entry, right);
            return new Spreadsheet(this, this.Create("spreadsheets/" + workspacePath, entry));
        }

        /// <summary>
        /// Create a private spreadsheet in the workspace specified by <paramref name="workspacePath"/>.
        /// </summary>
        /// <param name="workspacePath">The path of the destined workspace.</param>
        /// <param name="spreadsheetName">The name of the spreadsheet to be created.</param>
        /// <param name="note">The note of the spreadsheet to be created.</param>
        /// <returns>A <c>Spreadsheet</c> object.</returns>
        public Spreadsheet CreateSpreadsheet(string workspacePath, string spreadsheetName, string note)
        {
            return this.CreateSpreadsheet(workspacePath, spreadsheetName, note, PublicAccessRight.Private);
        }

        /// <summary>
        /// Import a spreadsheet into the workspace specified by <paramref name="workspacePath"/>.
        /// </summary>
        /// <param name="workspacePath">The path of the destined workspace.</param>
        /// <param name="spreadsheetName">The name of the spreadsheet to be created.</param>
        /// <param name="contentType">MIME type of the file to be imported.</param>
        /// <param name="stream">Input stream of the file to be imported.</param>
        /// <returns></returns>
        public Spreadsheet ImportSpreadsheet(string workspacePath, string spreadsheetName, string contentType, Stream stream)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.baseUri, "rest/1/spreadsheets/" + workspacePath));
            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.appId);
            req.ApiToken = this.apiToken;
            req.ContentType = contentType;
            req.SetRequestBody(stream);
            req.Headers["Slug"] = spreadsheetName;
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode == HttpStatusCode.Created)
            {
                return new Spreadsheet(this, res.ResponseAtomEntry);
            }
            else
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }

        /// <summary>
        /// Retrieve the spreadsheet specified by <paramref name="spreadsheetPath"/>.
        /// </summary>
        /// <param name="spreadsheetPath">Path of the spreadsheet to be retrieved.</param>
        /// <returns>A <c>Spreadsheet</c> object.</returns>
        public Spreadsheet GetSpreadsheet(string spreadsheetPath)
        {
            AtomEntry entry = this.QueryEntry("spreadsheets/" + spreadsheetPath);
            return new Spreadsheet(this, entry);
        }

        /// <summary>
        /// Delete the spreadsheet specified by <paramref name="spreadsheetPath"/>.
        /// </summary>
        /// <param name="spreadsheetPath">Path of the spreadsheet to be deleted.</param>
        /// <returns>A <c>Spreadsheet</c> object representing the spreadsheet deleted.</returns>
        public Spreadsheet DeleteSpreadsheet(string spreadsheetPath)
        {
            AtomEntry entry = this.Delete("spreadsheets/" + spreadsheetPath);
            return new Spreadsheet(this, entry);
        }

        /// <summary>
        /// Returns a list of spreadsheets in the workspace specified by the
        /// <paramref name="workspacePath"/>.
        /// </summary>
        /// <param name="workspacePath">Path of the workspace.</param>
        /// <returns>A <c>List</c> of <c>Spreadsheet</c>.</returns>
        public List<Spreadsheet> ListSpreadsheet(string workspacePath)
        {
            AtomFeed feed = this.Query("spreadsheets/" + workspacePath);
            List<Spreadsheet> list = new List<Spreadsheet>();
            foreach (AtomEntry entry in feed.Entries)
            {
                list.Add(new Spreadsheet(this, entry));
            }
            return list;
        }

        public List<Spreadsheet> ListActiveSpreadsheet(string workspacePath)
        {
            QueryOption option = new QueryOption();
            option["filter"] = "active";
            AtomFeed feed = this.Query("spreadsheets/" + workspacePath, option);
            List<Spreadsheet> list = new List<Spreadsheet>();
            foreach (AtomEntry entry in feed.Entries)
            {
                list.Add(new Spreadsheet(this, entry));
            }
            return list;
        }

        /// <summary>
        /// Returns a list of public spreadsheets in the workspace specified by the
        /// <paramref name="workspacePath"/>.
        /// </summary>
        /// <param name="workspacePath">Path of the workspace.</param>
        /// <returns>A <c>List</c> of <c>Spreadsheet</c>.</returns>
        public List<Spreadsheet> ListPublicSpreadsheet(string workspacePath)
        {
            AtomFeed feed = this.Query("public-spreadsheets/" + workspacePath);
            List<Spreadsheet> list = new List<Spreadsheet>();
            foreach (AtomEntry entry in feed.Entries)
            {
                list.Add(new Spreadsheet(this, entry));
            }
            return list;
        }

        public List<Workspace> ListMyWorkspace()
        {
            AtomFeed feed = this.Query("workspaces/_me");
            List<Workspace> list = new List<Workspace>();
            foreach (AtomEntry entry in feed.Entries)
            {
                list.Add(new Workspace(entry));
            }
            return list;
        }

        /// <summary>
        /// Unlock a password protected spreadsheet
        /// </summary>
        /// <param name="spreadsheetPath">Path of the spreadsheet</param>
        /// <param name="password">Password</param>
        public void UnlockSpreadsheet(string spreadsheetPath, string password)
        {
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["spreadsheet"] = spreadsheetPath;
            parameters["password"] = password;
            EditGridHttpResponse res = this.CallAction("spreadsheets/unlock-password", parameters);
            if (res.StatusCode != HttpStatusCode.OK)
            {
                throw new EditGridServiceException(res.ResponseText);
            }
        }
    }
}
